// Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved.
// This source file is part of the Cangjie project, licensed under Apache-2.0
// with Runtime Library Exception.
//
// See https://cangjie-lang.cn/pages/LICENSE for license information.

#include <memory>

#include "cangjie/AST/Walker.h"

#include "Platform/PathEqual.h"

#include "Util/Io.h"
#include "Util/FileSystem.h"

#include "CangjieTools/Compiler.h"
#include "CangjieTools/Cjpm.h"
#include "Deveco/CjpmProject.h"

#include "Migrators/DevecoMigrator.h"

using namespace CangjieMigrator;
using namespace Cangjie::AST;

DevecoMigrator::DevecoMigrator(
    Path defaultCangjieHome, Option option, std::string version, std::unique_ptr<Rules::RuleList> rules)
    : Migrator(option, std::move(rules)),
    defaultCangjieHome(std::move(defaultCangjieHome)),
    source(option.source), target(option.target)
{}

static bool JudgeKeepOnUpdate(const std::vector<Deveco::CjpmProject> &projects)
{
    std::cout << "Find out these " << projects.size() << " project." << std::endl;
    auto allNotHaveParameter = true;
    for (const auto &project : projects) {
        if (project.HasParameter()) {
            allNotHaveParameter = false;
            std::cout << "[" << project.name << "]" << project.path << ": Find run parameter success" << std::endl;
        } else {
            std::cout << "[" << project.name << "]" << project.path << ": Find run parameter failed" << std::endl;
        }
    }

    if (allNotHaveParameter) {
        std::cerr << "All project find run parameter failed, abort to update." << std::endl;
        return false;
    }

    return true;
}

bool DevecoMigrator::ConvertPackage(const Path &cangjieHome, UpdateInstance::Package &package)
{
    auto compiler = CangjieTools::Compiler(cangjieHome);

    // 1. 编译源文件的语义信息
    compiler.CompileSemaPackage(package);

    // 2. 遍历语法树，并根据更新规则更新
    const auto rootNodes = compiler.GetAstRootNodes();
    for (auto &rootNode : rootNodes) {
        if (!rootNode) {
            std::cerr << "File migrator encountered an AST root node of package " << package.GetPath()
                      << " is a nullptr, skipping traversal. " << std::endl;
            continue;
        }
        if (!rootNode->curFile || rootNode->curFile->fileName.empty()) {
            std::cerr << "Found an AST node of package " << package.GetPath()
                      << " does not belong to any file, skipping traversal." << std::endl;
            continue;
        }
        auto fileName = rootNode->curFile->fileName;
        const auto filePath = package.GetPath() / fileName;
        if (!exists(filePath)) {
            std::cerr << "The file " << filePath << " belonging to an AST node was not found, skipping traversal."
                      << std::endl;
            continue;
        }
        auto &file = package.GetFile(fileName);
        CangjieTools::Compiler::TraverseAst(rootNode, rules, file);
    }

    // 3. 修改ImportInfo
    package.ChangeImport();

    // 4. 将上一步完成的UpdateList根据修改的Position写入target
    if (!package.Generate(target / Util::LexicalRelative(package.GetPath(), source))) {
        std::cerr << "Project migrator generate " << package.GetPath() << " failed. " << std::endl;
        return false;
    }

    return true;
}

bool DevecoMigrator::ConvertDevecoCjpmProject(const Deveco::CjpmProject &project)
{
    const auto targetDirectory = Platform::TempDirectory();
    const auto cjpm = CangjieTools::Cjpm(project.runParameter);
    auto cangjieHome = defaultCangjieHome;
    for (const auto &v : project.runParameter.environment) {
        if (v.name == "CANGJIE_HOME") {
            cangjieHome = v.value;
        }
    }
    auto infos = cjpm.Build(project.path, targetDirectory.GetPath());
    if (infos.empty()) {
        std::cerr << "Cjpm build failed, please fix it and try again." << std::endl;
        return false;
    }
    std::cout << "Cjpm build end." << std::endl;

    infos.erase(std::remove_if(infos.begin(), infos.end(), [this](const PackageCompileInfo &info) {
            return !Util::IsSubDir(source, info.packagePath);
        }), infos.end());
    UpdateInstance::Project p = UpdateInstance::Project(project.path);

    p.SetPackages(infos);

    bool success = true;
    for (const auto &package : p.GetPackages()) {
        const auto &packagePath = package->GetPath();
        if (!Util::IsSubDir(source, packagePath)) {
            std::cout << "Package " << packagePath << " is not belongs to project, jump update." << std::endl;
            continue;
        }
        if (!ConvertPackage(cangjieHome, *package)) {
            std::cerr << "Convert package " << package->GetPath() <<
                         " of project " << project.name << " failed." << std::endl;
            success = false;
        }
    }

    std::cout << std::endl;
    if (p.NeedImportStdx()) {
        std::cout << "Some std packages used in the project have been moved to stdx. "
                     "Please download and add a reference to stdx." << std::endl;
    }

    if (success) {
        std::cout << "Convert project " << project.name << " at " << project.path << " success." << std::endl;
    } else {
        std::cout << "Convert project " << project.name << " at " << project.path << " failed." << std::endl;
    }
    return success;
}

static bool CaseInsensitiveEquals(const std::string &a, const std::string &b)
{
    if (a.size() != b.size()) {
        return false;
    }
    for (size_t i = 0; i < a.size(); ++i) {
        if (std::tolower(a[i]) != std::tolower(b[i])) {
            return false;
        }
    }
    return true;
}

static std::string NormalizeSeparators(std::string s)
{
    std::replace(s.begin(), s.end(), '\\', '/');
    return s;
}

static Path LexicallyNormal(const Path &p)
{
    Path out;
    for (const auto &part : p) {
        if (part == ".") {
            continue;
        }
        if (part == "..") {
            if (!out.empty() && out.filename() != "..") {
                out = out.parent_path();
            } else {
                out /= part;
            }
        } else {
            out /= part;
        }
    }
    if (out.empty()) {
        out = ".";
    }
    return out;
}

// Parse an environment variable value which is a full absolute path to id_defined.json.
// If the path matches the required suffix structure, return the upper directory above "build".
// Otherwise, return std::nullopt.
static std::optional<Path> GetParameterPath(const std::string &value)
{
    if (value.empty()) {
        return std::nullopt;
    }

    // Normalize separators to '/' for cross-platform parsing and normalize the path
    Path full = LexicallyNormal(Path(NormalizeSeparators(value)));
    if (!full.is_absolute()) { // Require an absolute path; if not absolute, return nullopt
        return std::nullopt;
    }

    // The expected suffix segments (from file name up to "build")
    static const char *pathTail[] = {
        "id_defined.json",
        "ids_map",
        "default",
        "res",
        "intermediates",
        "default",
        "build"
    };

    // Step upward one level at a time, validating each segment
    Path cur = full;
    for (size_t i = 0; i < std::size(pathTail); ++i) {
        if (cur.empty()) {
            return std::nullopt;
        }
        const auto name = cur.filename().string();
        // Case-insensitive comparison for cross-platform compatibility
        if (!CaseInsensitiveEquals(name, pathTail[i])) {
            return std::nullopt;
        }

        cur = cur.parent_path();
    }

    // At this point, "cur" is the directory above "build" (the one we want)
    if (cur.empty()) {
        return std::nullopt;
    }

    // Ensure the path exists and is a directory
    std::error_code ec;
    if (!exists(cur, ec) || ec) {
        return std::nullopt;
    }
    if (!is_directory(cur, ec) || ec) {
        return std::nullopt;
    }

    return LexicallyNormal(cur) / "src" / "main" / "cangjie";
}

static std::vector<std::optional<Path>> GetParameterPaths(const std::vector<Deveco::CjpmRunParameter> &parameters)
{
    auto result = std::vector<std::optional<Path>>(parameters.size());
    for (auto i = 0; i < parameters.size(); ++i) {
        std::string idDefined = "";
        for (const auto &e : parameters[i].environment) {
            if (e.name == "idDefined") {
                idDefined = e.value;
            }
        }
        if (idDefined.empty()) {
            result[i] = std::nullopt;
        }

        result[i] = GetParameterPath(idDefined);
    }
    return result;
}

bool DevecoMigrator::Convert()
{
    auto projects = Deveco::CjpmProject::GetAllFrom(source);
    if (projects.empty()) {
        std::cerr << "Can not find any cjpm projects in " << source << std::endl;
        return false;
    }

    const auto logFile = source / ".hvigor" / "outputs" / "build-logs" / "build.log";
    if (!exists(logFile)) {
        std::cerr << "Log file " << logFile << " does not exist. Please build project once first." << std::endl;
        return false;
    }

    const auto logs = Deveco::Log::ReadLogsAndSelectDebug(logFile);
    const auto cjpmParameters = Deveco::CjpmRunParameter::ParseFromLogs(logs);
    const auto parameterPaths = GetParameterPaths(cjpmParameters);

    for (auto &project : projects) {
        const auto &name = project.name;
        for (size_t i = 0; i < cjpmParameters.size(); ++i) {
            const auto &path = parameterPaths[i];
            if (!path.has_value()) {
                continue;
            }
            auto &p = cjpmParameters[i];
            if (name == p.name && Platform::PathEqual(project.path, path.value())) {
                project.runParameter = std::move(p);
                break;
            }
        }
    }

    if (!JudgeKeepOnUpdate(projects)) {
        return false;
    }

    for (const auto &project : projects) {
        ConvertDevecoCjpmProject(project);
    }

    return true;
}
